import { app, BrowserWindow, Tray, Menu, ipcMain, dialog, net, protocol, shell } from 'electron'
import { autoUpdater } from "electron-updater"
import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import url from 'node:url'
import store from './store'
import { getProjects, parseProject } from './projects'
import dayjs from 'dayjs'
import fs from 'fs-extra'
import contextMenu from 'electron-context-menu';
import AdmZip from 'adm-zip'
import { setupTitlebar, attachTitlebarToWindow } from "tk-electron-titlebar/main";
import { $t, toggleLocale, mergeProjectLocale } from './locales'
import log from 'electron-log/main';
log.initialize();
log.transports.file.level = "info";
console.log = log.log;
const require = createRequire(import.meta.url)
const __dirname = path.dirname(fileURLToPath(import.meta.url))

const originalFs = require('original-fs')

// The built directory structure
//
// ├─┬─┬ dist
// │ │ └── index.html
// │ │
// │ ├─┬ dist-electron
// │ │ ├── main.js
// │ │ └── preload.mjs
// │
process.env.APP_ROOT = path.join(__dirname, '..')

// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x
export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron')
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
export const DATA_PATH = path.join(app.getPath('userData'), 'data');
export const TEMP_PATH = path.join(app.getPath('userData'), 'temp');
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST
fs.ensureDirSync(DATA_PATH);
fs.ensureDirSync(TEMP_PATH);
fs.emptyDirSync(TEMP_PATH);

let win: BrowserWindow | null
let tray: Tray
let winMap: any = {}
setupTitlebar();

mergeProjectLocale();

contextMenu({
  showSaveImageAs: true,
  showSearchWithGoogle: false,
  showInspectElement: false,
  showCopyLink: false,
  showSelectAll: false,
  showCopyImage: false,
  labels: {
    saveImageAs: $t("menu.saveImageAs"),
  },
  prepend: (_defaultActions, parameters, current) => {
    const params = new URLSearchParams(parameters.linkURL.split("?").pop());
    const isExtension = parameters.linkURL.includes('/extension') && params.get("name") ? true : false;
    return [
      {
        label: $t('menu.open'),
        visible: isExtension,
        click: () => {
          pageExtension(null, params.get("name") || "")
        }
      },
      {
        label: $t('menu.star'),
        visible: isExtension && params.get("star") !== "true",
        click: () => {
          starExtension(null, params.get("name") || "")
        }
      },
      {
        label: $t('menu.unstar'),
        visible: isExtension && params.get("star") === "true",
        click: () => {
          unStarExtension(null, params.get("name") || "")
        }
      },
      {
        label: $t('menu.install'),
        visible: isExtension && params.get("installed") !== "true",
        click: () => {
          installExtension(null, params.get("name") || "", false)
        }
      },
      {
        label: $t('menu.uninstall'),
        visible: isExtension && params.get("installed") === "true",
        click: () => {
          unInstallExtension(null, params.get("name") || "")
        }
      },
      {
        label: $t('menu.upgrade'),
        visible: isExtension && params.get("upgradable") === "true",
        click: () => {
          upgradeExtension(null, params.get("name") || "")
        }
      },
      {
        label: $t('menu.devtools'),
        visible: parameters.mediaType === 'none',
        click: () => {
          (current as BrowserWindow).webContents.openDevTools({ mode: 'detach' })
        }
      },
      {
        label: "Development",
        visible: parameters.mediaType === 'none',
        click: () => {
          pageDevelopment()
        }
      },
    ]
  }
});

function createWindow() {
  win = new BrowserWindow({
    icon: path.join(process.env.VITE_PUBLIC, 'favicon.ico'),
    title: "Toy-Kit",
    frame: false,
    show: false,
    webPreferences: {
      preload: path.join(__dirname, 'preload.mjs'),
    },
  })
  winMap.main = win.id;
  // Test active push message to Renderer-process.
  win.webContents.on('did-finish-load', () => {
    win?.webContents.send('projects', { packages: store.get("projects") })
    loadData()
    if (store.get("config.autoUpdater") === true) {
      checkUpdate(null, true)
    }
    if (store.get("config.openAtTray") !== true) {
      winShow();
    }
  })
  win.on('close', (e) => {
    win?.hide();
    e.preventDefault();
  })
  attachTitlebarToWindow(win);
  Menu.setApplicationMenu(null);
  tray = new Tray(path.join(process.env.VITE_PUBLIC, 'favicon.ico'))
  tray.setToolTip('Toy-Kit')
  tray.on("click", winShow)
  trayMenu()
  createPreload();
  protocolHandle()
  handleUpdate()
  setTimeout(() => { loadData() }, 10 * 60 * 1000)
  if (VITE_DEV_SERVER_URL) {
    win.loadURL(VITE_DEV_SERVER_URL)
  } else {
    win.loadFile(path.join(RENDERER_DIST, 'index.html'))
  }
}
const trayMenu = () => {
  let projects: any = store.get("projects") || []
  let menus: any = []
  for (let i in projects) {
    if (projects[i].star) {
      menus.push({
        label: $t(`${projects[i].name}.title`),
        click: () => { pageExtension(null, projects[i].name) },
      })
    }
  }
  if (menus.length > 0) {
    menus.push({ type: 'separator' })
  }
  menus.push({ label: $t('tray.setting'), click: pageSetting })
  menus.push({ label: $t('tray.quit'), click: winClose })
  const contextMenu = Menu.buildFromTemplate(menus)
  tray.setContextMenu(contextMenu)
}
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
    win = null
  }
})

app.on('activate', () => {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})

if (app.requestSingleInstanceLock()) {
  app.whenReady().then(createWindow)
} else {
  app.quit()
}

const createPreload = () => {
  ipcMain.on("page", newPage)
  ipcMain.on("setStore", setStore)
  ipcMain.handle("getStore", getStore)
  ipcMain.on("setConfig", setConfig)
  ipcMain.on("refreshConfig", refreshConfig)
  ipcMain.on("pageExtension", pageExtension)
  ipcMain.on("installExtension", installExtension)
  ipcMain.on("loadData", loadData)
  ipcMain.handle("server", handleServer)
  ipcMain.on("pageSetting", pageSetting)
  ipcMain.handle("initialize", initialize)
  ipcMain.on("updateHeight", updateHeight)
  ipcMain.on("winShow", winShow)
  ipcMain.on("checkUpdate", checkUpdate)
  ipcMain.on("openExternal", openExternal)
}

const pageDevelopment = () => {
  dialog.showOpenDialog({ defaultPath: store.get("config.defaultPath") as any, properties: ['openDirectory'] }).then((res) => {
    if (res && res.filePaths && res.filePaths.length > 0) {
      let dir = res.filePaths[0]
      store.set("config.defaultPath", path.dirname(dir))
      let obj = fs.readJSONSync(path.join(dir, "package.json"));
      let pkg: any = parseProject(obj, {})
      pkg.screen = pkg.screen || { width: 800, height: 600 }
      newPage(null, {
        to: "extension",
        query: { name: pkg.name, version: pkg.version, mode: pkg.mode, fullscreen: pkg.fullscreen, dev: true },
        width: pkg.screen.width || 800,
        height: pkg.screen.height || 600,
      })
    }
  })
}
const openExternal = (_: any, url: string) => {
  shell.openExternal(url)
}
const winClose = () => {
  app.exit(0);
}
const winShow = () => {
  win?.show()
}
const updateHeight = (_: any, height: number) => {
  let w = BrowserWindow.getFocusedWindow()
  if (!w) return
  let bounds = w.getBounds()
  w.setBounds({ y: bounds.y + (bounds.height - height) / 2, height: height })
}
const initialize = () => {
  return {
    config: store.get("config"),
    locales: store.get("locales")
  }
}
const handleServer = (_: any, options: any) => {
  return new Promise((resolve, reject) => {
    import(url.pathToFileURL(path.join(DATA_PATH, options.name, options.version, "app.asar", "server.cjs")).toString()).then((modules: any) => {
      modules.default(options.options).then((result: any) => {
        resolve(result)
      }).catch((err: any) => {
        reject(err)
      })
    }).catch((err: any) => {
      reject(err)
    })
  })
}

let installs: any = {}

const handleInstall = (pkg: any, running: boolean) => {
  win?.webContents.send("progress", { name: pkg.name, finish: 0, total: 0, percent: 0, running: running })
  return new Promise((resolve) => {
    fs.ensureDirSync(TEMP_PATH)
    let tempFilename = path.join(TEMP_PATH, `${pkg.name}-${pkg.version}-${new Date().getTime()}`)
    const install = (f: number, t: number) => {
      let dir = path.join(DATA_PATH, pkg.name, pkg.version)
      fs.ensureDirSync(dir)
      const zip = new AdmZip(tempFilename, { fs: originalFs });
      zip.extractAllTo(dir, true);
      win?.webContents.send("progress", { name: pkg.name, finish: f, total: t, percent: 101, running: running })
      resolve(true)
    }
    const request = net.request(`https://unpkg.com/@toy-kit/${pkg.name}@${pkg.version}/dist/app.zip`)
    request.on('response', (response: any) => {
      if (response.statusCode !== 200) {
        fs.removeSync(tempFilename)
        alert(response.statusMessage)
        win?.webContents.send("progress", { name: pkg.name, err: response.statusMessage, percent: -1 })
        resolve(false)
        return
      }
      let total = Number(response.headers["content-length"] || 0)
      let finish = 0;
      response.on('data', (chunk: Buffer) => {
        try {
          fs.appendFileSync(tempFilename, chunk)
        } catch (err: any) {
          alert(err.message)
          win?.webContents.send("progress", { name: pkg.name, percent: -1 })
          resolve(false)
          return
        }
        finish += chunk.length
        win?.webContents.send("progress", { name: pkg.name, finish: finish, total: total, percent: parseInt((finish / total * 100).toString()) })
      })
      response.on('end', () => {
        win?.webContents.send("progress", { name: pkg.name, finish: finish, total: total, percent: parseInt((finish / total * 100).toString()) })
        install(finish, total)
      })
    })
    request.on('error', (err: Error) => {
      fs.removeSync(tempFilename)
      alert(err.message)
      win?.webContents.send("progress", { name: pkg.name, percent: -1 })
      resolve(false)
      return
    })
    request.end()
  })
}
const upgradeExtension = async (_: any, name: string) => {
  let pkg: any = store.get(`projects.${name}`, {})
  if (!pkg.latest) {
    alert("没有更新")
    return
  }
  if (await handleInstall(pkg.latest, false)) {
    pkg.latest.installed = true
    pkg.latest.upgradable = false;
    store.set(`projects.${name}`, pkg.latest)
  }
}

const installExtension = async (_: any, name: string, running: boolean) => {
  let pkg: any = store.get(`projects.${name}`, {})
  if (await handleInstall(pkg, running)) {
    store.set(`projects.${pkg.name}.installed`, true)
  }
}
const unInstallExtension = (_: any, name: string) => {
  store.set(`projects.${name}.installed`, false)
  loadData()
  alert("卸载成功")
}
const starExtension = (_: any, name: string) => {
  store.set(`projects.${name}.star`, true)
  loadData()
  trayMenu()
}
const unStarExtension = (_: any, name: string) => {
  store.set(`projects.${name}.star`, false)
  loadData()
  trayMenu()
}
const installCheck = (name: string, version: string) => {
  return fs.existsSync(path.join(DATA_PATH, name, version, "app.asar"));
}
const pageExtension = (e: any, name: string) => {
  let pkg: any = store.get(`projects.${name}`, {})
  if (installs[pkg.name]) {
    return
  }
  if (!installCheck(pkg.name, pkg.version)) {
    installExtension(e, pkg.name, true)
    return
  }
  let icon = path.join(process.env.VITE_PUBLIC, 'favicon.ico')
  if (pkg.installed) {
    icon = path.join(DATA_PATH, pkg.name, pkg.version, "app.asar", "dist", "favicon.png")
  }
  pkg.screen = pkg.screen || { width: 800, height: 600 }
  newPage(e, {
    to: "extension",
    query: { name: pkg.name, version: pkg.version, mode: pkg.mode, fullscreen: pkg.fullscreen },
    icon: icon,
    title: pkg.name,
    resizable: pkg.fullscreen,
    width: pkg.screen.width || 800,
    height: pkg.screen.height || 600,
  })
}
const fromWinMap = (to: string) => {
  if (!winMap[to]) {
    return null
  }
  return BrowserWindow.fromId(winMap[to])
}
const pageSetting = (e: any) => {
  let w = fromWinMap("setting")
  if (w) {
    w.show()
    w.focus()
    return
  }
  newPage(e, {
    to: "setting",
    query: { fullscreen: false },
    icon: path.join(process.env.VITE_PUBLIC, 'favicon.ico'),
    title: $t("setting.setting"),
    resizable: false,
    width: 500,
    height: 450,
  })
}
const setOpenAtLogin = (value: any) => {
  app.setLoginItemSettings({
    openAtLogin: value,
  });
}
const refreshConfig = (_: any) => {
  let config = store.get("config")
  BrowserWindow.getAllWindows().forEach(w => {
    w?.webContents.send("config", config)
  })
}
const setConfig = (e: any, key: string, value: any) => {
  if (key === "locale") {
    toggleLocale(value);
    trayMenu()
  } else if (key === "openAtLogin") {
    setOpenAtLogin(value)
  }
  store.set(`config.${key}`, value)
  refreshConfig(e)
}
const setStore = (_: any, key: string, value: any) => {
  store.set(key, value)
}
const getStore = (_: any, key: string, defaultValue: any) => {
  return store.get(key, defaultValue);
}

const newPage = (_: any, options: any) => {
  let child = new BrowserWindow({
    icon: options.icon || path.join(process.env.VITE_PUBLIC, 'favicon.ico'),
    title: options.title,
    show: false,
    width: options.width,
    height: options.height,
    resizable: options.resizable === undefined ? true : options.resizable,
    frame: false,
    webPreferences: {
      preload: path.join(__dirname, 'preload.mjs'),
    },
  })
  winMap[options.to] = child.id;
  attachTitlebarToWindow(child);
  child.once('ready-to-show', () => {
    child.show()
  })
  child.on('closed', () => {
    delete winMap[options.to]
    if (options.query.mode === 'server') {
      handleServer(null, { name: options.query.name, version: options.query.version, options: { event: 'close' } }).catch((err) => {
        console.error(err)
      })
    }
  })
  options.query = options.query || {}
  if (VITE_DEV_SERVER_URL) {
    child.loadURL(VITE_DEV_SERVER_URL + `?${new URLSearchParams(options.query).toString()}#/${options.to}`)
  } else {
    child.loadFile(path.join(RENDERER_DIST, 'index.html'), { hash: options.to, query: options.query })
  }
}

const loadData = () => {
  let updateAt: any = store.get("projects_update_at", 0)
  let packages = store.get("projects")
  if (packages && dayjs(updateAt).add(10, 'minute').isAfter(dayjs())) {
    win?.webContents.send('projects', { packages: packages })
    return;
  }
  getProjects().then((res) => {
    win?.webContents.send('projects', { packages: res })
  })
}
const getFocusedWindow = () => {
  let w = BrowserWindow.getFocusedWindow()
  if (w) {
    return w;
  }
  return win
}
const alert = (message: string) => {
  dialog.showMessageBox(getFocusedWindow() as BrowserWindow, { message: message, title: $t("message_title") })
}
const question = (message: string) => {
  return new Promise((resolve, reject) => {
    dialog.showMessageBox(getFocusedWindow() as BrowserWindow, {
      message: message,
      title: $t("message_title"),
      type: "question",
      buttons: [$t("yes"), $t("no")],
      noLink: true,
    }).then((res) => {
      if (res && res.response === 0) {
        resolve(true)
      } else {
        reject(false)
      }
    }).catch(() => {
      reject(false)
    })
  })
}
let updaterAlert = false
const handleUpdate = () => {
  autoUpdater.logger = log
  autoUpdater.autoDownload = false
  autoUpdater.setFeedURL("https://gitee.com/toy-kit/toy-kit/releases/download/latest/")
  autoUpdater.on('error', (err: any, message?: string) => {
    if (updaterAlert) {
      console.log(err)
    } else {
      alert(message || $t("updater.error"))
    }
  })
  autoUpdater.on("update-available", () => {
    question($t("updater.available")).then(() => {
      getFocusedWindow()?.webContents.send("updaterProgress", { total: 0, transferred: 0, bytesPerSecond: 0, percent: 0 })
      autoUpdater.downloadUpdate()
    })
  })
  autoUpdater.on('update-not-available', (info: any) => {
    if (updaterAlert) {
      console.log(info)
    } else {
      alert($t("updater.notavailable"))
    }
  })
  autoUpdater.on("download-progress", (progressObj) => {
    getFocusedWindow()?.webContents.send("updaterProgress", progressObj)
  })
  autoUpdater.on("update-downloaded", () => {
    question($t("updater.downloaded")).then(() => {
      autoUpdater.quitAndInstall()
    })
  })
}

const checkUpdate = (_: any, auto: boolean) => {
  updaterAlert = auto || false
  autoUpdater.checkForUpdates()
}

const originalFsRmdirSync = (filePath: string) => {
  try {
    if (originalFs.existsSync(filePath)) {
      const files = originalFs.readdirSync(filePath)
      files.forEach((file: any) => {
        const nextFilePath = `${filePath}/${file}`
        const states = originalFs.statSync(nextFilePath)
        if (states.isDirectory()) {
          originalFsRmdirSync(nextFilePath)
        } else {
          originalFs.unlinkSync(nextFilePath)
        }
      })
      fs.rmdirSync(filePath)
      return "";
    }
  } catch (e: any) {
    console.error(e.message)
    return e.message;
  }
}

const protocolHandle = () => {
  protocol.handle('atom', async (request) => {
    let filePath = request.url.slice('atom://'.length)
    let arr = filePath.split('/')
    return await net.fetch(url.pathToFileURL(filePath.replace(`${arr[0]}/${arr[1]}/dist`, path.join(DATA_PATH, arr[0], arr[1], "app.asar"))).toString())
  })
}

const clearUninstall = () => {
  const dirs = fs.readdirSync(DATA_PATH)
  dirs.forEach((dir: string) => {
    if (store.get(`projects.${dir}.installed`, false) !== true) {
      originalFsRmdirSync(path.join(DATA_PATH, dir))
    }
  })
}
setConfig(null, "version", app.getVersion());
clearUninstall()